package server;

import java.io.IOException;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;

import iface.Envelope;
import iface.Heartbeat;
import iface.Kill;
import iface.Logger;
import iface.Location;
import iface.Registration;
import iface.Status;
import iface.HvZUser;
import iface.Message;
import iface.UserTypes;

/**
 * Handles connections given by the server
 * 
 * @author Jeff
 */
public class HvZDispacher extends Thread {
    private Envelope env;
    private ClientQueryable db;
    private ConnectionState connState;
    private Map<UserCache.CacheType, UserCache> caches;

    /**
     * Constructs a new HvZDispatcher
     * 
     * @param connState
     *            the connection to the client
     * @param env
     *            the envelope to handle
     * @param caches
     *            the cache of humans and zombies
     */
    public HvZDispacher(ConnectionState connState, Envelope env,
            Map<UserCache.CacheType, UserCache> caches) {
        this.db = Database.getInstance();
        this.env = env;
        this.connState = connState;
        this.caches = caches;
    }

    /**
     * Runs the thread. Call with start()
     */
    public void run() {
        InetAddress clientIp = connState.getClientSocket().getInetAddress();
        if (env.getContents() == null) {
            Logger.error("Uh oh! The contents of the received envelope is null: "
                    + env);
        } else if (env.getContents() instanceof Kill) {
            Kill kill = (Kill) env.getContents();
            Logger.recv(clientIp, "Kill", kill);
            executeKillRequest(kill);
        } else if (env.getContents() instanceof Location) {
            Location loc = (Location) env.getContents();
            Logger.recv(clientIp, "Location", loc);
            executeLocationRequest(loc);
        } else if (env.getContents() instanceof Registration) {
            Registration reg = (Registration) env.getContents();
            Logger.recv(clientIp, "Registration", reg);
            executeRegistrationRequest(reg);
        } else if (env.getContents() instanceof Heartbeat) {
            Heartbeat hb = (Heartbeat) env.getContents();
            Logger.recv(clientIp, "Heartbeat", hb);
            executeHeartbeat(hb);
        } else {
            Logger.error("Unable to determine contents of the received envelope: "
                    + env.getContents());
        }
    }

    /*
     * Given a Heartbeat object the server blindly replies with a success, which
     * indicates to the sender that the server is still alive.
     */
    private void executeHeartbeat(Heartbeat hb) {
        Status status = new Status(Status.Result.SUCCESS, Message.MsgType.HEARTBEAT);
        replyAndCleanup(new Envelope("server", status));
    }

    /*
     * Given the Kill Object, the kill is executed and a reply is sent to the
     * client. Only zombies can initiate kills and humans be victims. Players
     * cannot kill themselves. If the kill succeeds, the caches are altered.
     */
    private void executeKillRequest(Kill kill) {
        HvZUser attacker = db.getUser(kill.getUsername());
        String victim = kill.getUserToKill();
        Status status = null;

        // Check that the user exists in the database
        if (attacker == null) {
            status = new Status(Status.Result.ERROR, Message.MsgType.KILL);
            status.setMessage(kill.getUsername() + " does not exist!");
        } else if (attacker.isZombie()) {
            status = db.changeToZombie(victim);

            // Update cache upon successful database update.
            if (status.getMessageStatus().equals(Status.Result.SUCCESS)) {
                UserCache humanCache = caches.get(UserCache.CacheType.HUMAN);
                UserCache zombieCache = caches.get(UserCache.CacheType.ZOMBIE);
                if (humanCache.isUserCached(victim)) {
                    HvZUser user = humanCache.removeUser(victim);
                    user.killUser();
                    zombieCache.addUser(user);
                }
            }
        } else if (attacker.getUserType().equals(UserTypes.OBSERVER)) {
            // User is an observer. Cannot perform kills.
            status = new Status(Status.Result.ERROR, Message.MsgType.KILL);
            status.setMessage("Observers are not allowed to kill others");
        } else {
            // Case that the attacker is human, and therefore cannot kill
            status = new Status(Status.Result.ERROR, Message.MsgType.KILL);
            status.setMessage(attacker.getUsername()
                    + " cannot spread the infection!");
        }
        replyAndCleanup(new Envelope("server", status));
    }

    /*
     * Given the Registration object the player is registered into the DB. The
     * registration will succeed if the username or phone number is not already
     * in use.
     */
    private void executeRegistrationRequest(Registration reg) {
        Status status = db.registerNewUser(reg);

        if (status == null) {
            status = new Status(Status.Result.ERROR,
                    Message.MsgType.REGISTRATION);
            status.setMessage("Failed to connect to the database");
        }

        replyAndCleanup(new Envelope("server", status));
    }

    private void executeLocationRequest(Location loc) {
        Status status = new Status(Status.Result.SUCCESS,
                Message.MsgType.LOCATION);
        String username = loc.getUsername();

        // Update location info for a cached user.
        UserCache humanCache = caches.get(UserCache.CacheType.HUMAN);
        UserCache zombieCache = caches.get(UserCache.CacheType.ZOMBIE);
        if (humanCache.isUserCached(username)) {
            humanCache.updateLocation(loc);
            status.setLocationList(humanCache.getCacheValues());

        } else if (zombieCache.isUserCached(username)) {
            zombieCache.updateLocation(loc);
            status.setLocationList(zombieCache.getCacheValues());

            // Not cached yet, load into cache and update!
            // Or user is observer, so send it all locations
        } else {
            HvZUser user = db.getUser(username);
            if (user != null) {
                user.updateLocation(loc);
                if (user.isZombie()) {
                    zombieCache.addUser(user);
                    status.setLocationList(zombieCache.getCacheValues());
                } else if (user.getUserType().equals(UserTypes.OBSERVER)) {
                    List<HvZUser> all_locs = humanCache.getCacheValues();
                    all_locs.addAll(zombieCache.getCacheValues());
                    status.setLocationList(all_locs);
                } else {
                    humanCache.addUser(user);
                    status.setLocationList(humanCache.getCacheValues());
                }
            } else {
                status = new Status(Status.Result.ERROR,
                        Message.MsgType.LOCATION);
                status.setMessage("Cannot update location since user doesn't exist in database.");
            }
        }

        replyAndCleanup(new Envelope("server", status));
    }

    /*
     * Replies the Status to the client and closes the connection
     */
    private void replyAndCleanup(Envelope env) {
        try {
            connState.getOutStream().writeObject(env);
            Logger.log("SENT: " + env);
        } catch (IOException e) {
            Logger.error("Unable to write response object to client: "
                    + e.toString());
        }
        try {
            connState.getInStream().close();
        } catch (IOException e1) {
            Logger.error("Unable to cleanup input stream: " + e1.toString());
        }
        try {
            connState.getOutStream().flush();
            connState.getOutStream().close();
        } catch (IOException e2) {
            Logger.error("Unable to cleanup output stream: " + e2.toString());
        }
        try {
            connState.getClientSocket().close();
        } catch (IOException e3) {
            Logger.error("Unable to close client socket: " + e3.toString());
        }
    }
}
